package cn.xzc.job.config;

import cn.xzc.job.JobScheduler;
import cn.xzc.job.ScheduledJobHolder;
import cn.xzc.job.Trigger;
import cn.xzc.job.cocurrent.ConcurrentJobScheduler;
import cn.xzc.job.constant.Constant;
import cn.xzc.job.pojo.Job;
import cn.xzc.job.pojo.ScheduledJob;
import cn.xzc.job.type.TriggerJob;
import cn.xzc.job.type.cron.CronJob;
import cn.xzc.job.type.cron.CronTrigger;
import cn.xzc.job.type.interval.FixedDelayJob;
import cn.xzc.job.type.interval.FixedRateJob;
import cn.xzc.job.type.interval.IntervalJob;
import cn.xzc.job.util.CollectionUtils;

import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;


/**
 * 定时任务注册到定时任务执行队列中
 *
 * @author xzc
 * 当前时间 2024-02-12 21:30:00
 */
public class ScheduledJobRegistrar implements ScheduledJobHolder {

    private final Map<Job, ScheduledJob> unresolvedJobs = new HashMap<>(16);
    private final Set<ScheduledJob> scheduledJobs = new LinkedHashSet<>(16);
    
    private JobScheduler jobScheduler;
    
    private ScheduledExecutorService localExecutor;
    
    private List<TriggerJob> triggerJobs;
    
    private List<CronJob> cronJobs;
    
    private List<FixedRateJob> fixedRateJobs;
    
    private List<FixedDelayJob> fixedDelayJobs;

    /**
     * Set the {@link JobScheduler} to register scheduled tasks with.
     */
    public void setJobScheduler(JobScheduler jobScheduler) {
        this.jobScheduler = jobScheduler;
    }

    /**
     * Return the {@link JobScheduler} instance for this registrar (may be {@code null}).
     */
    
    public JobScheduler getScheduler() {
        return this.jobScheduler;
    }

    /**
     * Set the {@link JobScheduler} to register scheduled tasks with, or a
     * {@link java.util.concurrent.ScheduledExecutorService} to be wrapped as a
     * {@code JobScheduler}.
     */
    public void setScheduler(
                             Object scheduler) {
        if (scheduler == null) {
            this.jobScheduler = null;
        } else if (scheduler instanceof JobScheduler) {
            this.jobScheduler = (JobScheduler) scheduler;
        } else if (scheduler instanceof ScheduledExecutorService) {
            this.jobScheduler = new ConcurrentJobScheduler(((ScheduledExecutorService) scheduler));
        } else {
            throw new IllegalArgumentException("Unsupported scheduler type: " + scheduler.getClass());
        }
    }

    /**
     * Specify triggered tasks as a Map of Runnables (the tasks) and Trigger objects
     * (typically custom implementations of the {@link Trigger} interface).
     */
    public void setTriggerJobs(Map<Runnable, Trigger> triggerJobs) {
        this.triggerJobs = new ArrayList<>();
        triggerJobs.forEach((job, trigger) -> addTriggerJob(new TriggerJob(job, trigger)));
    }

    /**
     * Specify triggered tasks as a list of {@link TriggerJob} objects. Primarily used
     * by {@code <task:*>} namespace parsing.
     */
    public void setTriggerJobsList(List<TriggerJob> triggerJobs) {
        this.triggerJobs = triggerJobs;
    }

    /**
     * Get the trigger tasks as an unmodifiable list of {@link TriggerJob} objects.
     *
     * @return the list of tasks (never {@code null})
     */
    public List<TriggerJob> getTriggerJobList() {
        return (this.triggerJobs != null ? Collections.unmodifiableList(this.triggerJobs) :
                Collections.emptyList());
    }

    /**
     * Specify triggered tasks as a Map of Runnables (the tasks) and cron expressions.
     *
     * @see CronTrigger
     */
    public void setCronJobs(Map<Runnable, String> cronJobs) {
        this.cronJobs = new ArrayList<>();
        cronJobs.forEach(this::addCronJob);
    }

    /**
     * Specify triggered tasks as a list of {@link CronJob} objects. Primarily used by
     * {@code <task:*>} namespace parsing.
     */
    public void setCronJobsList(List<CronJob> cronJobs) {
        this.cronJobs = cronJobs;
    }

    /**
     * Get the cron tasks as an unmodifiable list of {@link CronJob} objects.
     *
     * @return the list of tasks (never {@code null})
     */
    public List<CronJob> getCronJobList() {
        return (this.cronJobs != null ? Collections.unmodifiableList(this.cronJobs) :
                Collections.emptyList());
    }

    /**
     * Specify triggered tasks as a Map of Runnables (the tasks) and fixed-rate values.
     *
     * @see JobScheduler#scheduleAtFixedRate(Runnable, long)
     */
    public void setFixedRateJobs(Map<Runnable, Long> fixedRateJobs) {
        this.fixedRateJobs = new ArrayList<>();
        fixedRateJobs.forEach(this::addFixedRateJob);
    }

    /**
     * Specify fixed-rate tasks as a list of {@link IntervalJob} objects. Primarily used
     * by {@code <task:*>} namespace parsing.
     */
    public void setFixedRateJobsList(List<FixedRateJob> fixedRateJobs) {
        this.fixedRateJobs = fixedRateJobs;
    }

    /**
     * Get the fixed-rate tasks as an unmodifiable list of {@link IntervalJob} objects.
     *
     * @return the list of tasks (never {@code null})
     */
    public List<IntervalJob> getFixedRateJobList() {
        return (this.fixedRateJobs != null ? Collections.unmodifiableList(this.fixedRateJobs) :
                Collections.emptyList());
    }

    /**
     * Specify triggered tasks as a Map of Runnables (the tasks) and fixed-delay values.
     *
     * @see JobScheduler#scheduleWithFixedDelay(Runnable, long)
     */
    public void setFixedDelayJobs(Map<Runnable, Long> fixedDelayJobs) {
        this.fixedDelayJobs = new ArrayList<>();
        fixedDelayJobs.forEach(this::addFixedDelayJob);
    }

    /**
     * Specify fixed-delay tasks as a list of {@link IntervalJob} objects. Primarily used
     * by {@code <task:*>} namespace parsing.
     */
    public void setFixedDelayJobsList(List<FixedDelayJob> fixedDelayJobs) {
        this.fixedDelayJobs = fixedDelayJobs;
    }

    /**
     * Get the fixed-delay tasks as an unmodifiable list of {@link IntervalJob} objects.
     *
     * @return the list of tasks (never {@code null})
     */
    public List<IntervalJob> getFixedDelayJobList() {
        return (this.fixedDelayJobs != null ? Collections.unmodifiableList(this.fixedDelayJobs) :
                Collections.emptyList());
    }


    /**
     * Add a Runnable task to be triggered per the given {@link Trigger}.
     *
     * @see JobScheduler#scheduleAtFixedRate(Runnable, long)
     */
    public void addTriggerJob(Runnable job, Trigger trigger) {
        addTriggerJob(new TriggerJob(job, trigger));
    }

    /**
     * Add a {@code TriggerJob}.
     *
     * @see JobScheduler#scheduleAtFixedRate(Runnable, long)
     */
    public void addTriggerJob(TriggerJob job) {
        if (this.triggerJobs == null) {
            this.triggerJobs = new ArrayList<>();
        }
        this.triggerJobs.add(job);
    }

    /**
     * Add a {@link Runnable} task to be triggered per the given cron {@code expression}.
     * <p>As of Spring Framework 5.2, this method will not register the task if the
     * {@code expression} is equal to {@link # CRON_DISABLED}.
     */
    public void addCronJob(Runnable job, String expression) {
        if (!Constant.CRON_DISABLED.equals(expression)) {
            addCronJob(new CronJob(job, expression));
        }
    }

    /**
     * Add a {@link CronJob}.
     */
    public void addCronJob(CronJob job) {
        if (this.cronJobs == null) {
            this.cronJobs = new ArrayList<>();
        }
        this.cronJobs.add(job);
    }

    /**
     * Add a {@code Runnable} task to be triggered at the given fixed-rate interval.
     *
     * @see JobScheduler#scheduleAtFixedRate(Runnable, long)
     */
    public void addFixedRateJob(Runnable job, long interval) {
        addFixedRateJob(new FixedRateJob(job, interval, 0));
    }

    /**
     * Add a fixed-rate {@link FixedRateJob}.
     *
     * @see JobScheduler#scheduleAtFixedRate(Runnable, long)
     */
    public void addFixedRateJob(FixedRateJob job) {
        if (this.fixedRateJobs == null) {
            this.fixedRateJobs = new ArrayList<>();
        }
        this.fixedRateJobs.add(job);
    }

    /**
     * Add a Runnable task to be triggered with the given fixed delay.
     *
     * @see JobScheduler#scheduleWithFixedDelay(Runnable, long)
     */
    public void addFixedDelayJob(Runnable job, long delay) {
        addFixedDelayJob(new FixedDelayJob(job, delay, 0));
    }

    /**
     * Add a fixed-delay {@link FixedDelayJob}.
     *
     * @see JobScheduler#scheduleWithFixedDelay(Runnable, long)
     */
    public void addFixedDelayJob(FixedDelayJob job) {
        if (this.fixedDelayJobs == null) {
            this.fixedDelayJobs = new ArrayList<>();
        }
        this.fixedDelayJobs.add(job);
    }


    /**
     * Return whether this {@code ScheduledJobRegistrar} has any tasks registered.
     */
    public boolean hasJobs() {
        return (!CollectionUtils.isEmpty(this.triggerJobs) ||
                !CollectionUtils.isEmpty(this.cronJobs) ||
                !CollectionUtils.isEmpty(this.fixedRateJobs) ||
                !CollectionUtils.isEmpty(this.fixedDelayJobs));
    }


    /**
     * Calls {@link #scheduleJobs()} at bean construction time.
     */
    public void init() {
        scheduleJobs();
    }

    /**
     * Schedule all registered tasks against the underlying
     * {@linkplain #setJobScheduler(JobScheduler) task scheduler}.
     */
    @SuppressWarnings("deprecation")
    protected void scheduleJobs() {
        if (this.jobScheduler == null) {
            this.localExecutor = Executors.newSingleThreadScheduledExecutor();
            this.jobScheduler = new ConcurrentJobScheduler(this.localExecutor);
        }
        if (this.triggerJobs != null) {
            for (TriggerJob job : this.triggerJobs) {
                addScheduledJob(scheduleTriggerJob(job));
            }
        }
        if (this.cronJobs != null) {
            for (CronJob job : this.cronJobs) {
                addScheduledJob(scheduleCronJob(job));
            }
        }
        if (this.fixedRateJobs != null) {
            for (FixedRateJob job : this.fixedRateJobs) {
                addScheduledJob(scheduleFixedRateJob(job));
            }
        }
        if (this.fixedDelayJobs != null) {
            for (FixedDelayJob job : this.fixedDelayJobs) {
                addScheduledJob(scheduleFixedDelayJob(job));
            }
        }
    }

    private void addScheduledJob(
                                 ScheduledJob job) {
        if (job != null) {
            this.scheduledJobs.add(job);
        }
    }


    /**
     * Schedule the specified trigger task, either right away if possible
     * or on initialization of the scheduler.
     *
     * @return a handle to the scheduled task, allowing to cancel it
     */
    
    public ScheduledJob scheduleTriggerJob(TriggerJob job) {
        ScheduledJob scheduledJob = this.unresolvedJobs.remove(job);
        boolean newJob = false;
        if (scheduledJob == null) {
            scheduledJob = new ScheduledJob(job);
            newJob = true;
        }
        if (this.jobScheduler != null) {
            scheduledJob.setFuture(this.jobScheduler.schedule(job.getRunnable(), job.getTrigger()));
        } else {
            addTriggerJob(job);
            this.unresolvedJobs.put(job, scheduledJob);
        }
        return (newJob ? scheduledJob : null);
    }

    /**
     * Schedule the specified cron task, either right away if possible
     * or on initialization of the scheduler.
     *
     * @return a handle to the scheduled task, allowing to cancel it
     * (or {@code null} if processing a previously registered task)
     */
    
    public ScheduledJob scheduleCronJob(CronJob job) {
        ScheduledJob scheduledJob = this.unresolvedJobs.remove(job);
        boolean newJob = false;
        if (scheduledJob == null) {
            scheduledJob = new ScheduledJob(job);
            newJob = true;
        }
        if (this.jobScheduler != null) {
            scheduledJob.setFuture(this.jobScheduler.schedule(job.getRunnable(), job.getTrigger()));
        } else {
            addCronJob(job);
            this.unresolvedJobs.put(job, scheduledJob);
        }
        return (newJob ? scheduledJob : null);
    }


    /**
     * Schedule the specified fixed-rate task, either right away if possible
     * or on initialization of the scheduler.
     *
     * @return a handle to the scheduled task, allowing to cancel it
     * (or {@code null} if processing a previously registered task)
     */
    
    public ScheduledJob scheduleFixedRateJob(FixedRateJob job) {
        ScheduledJob scheduledJob = this.unresolvedJobs.remove(job);
        boolean newJob = false;
        if (scheduledJob == null) {
            scheduledJob = new ScheduledJob(job);
            newJob = true;
        }
        if (this.jobScheduler != null) {
            if (job.getInitialDelay() > 0) {
                Date startTime = new Date(this.jobScheduler.getClock().millis() + job.getInitialDelay());
                scheduledJob.setFuture(this.jobScheduler.scheduleAtFixedRate(job.getRunnable(), startTime, job.getInterval()));
            } else {
                scheduledJob.setFuture(this.jobScheduler.scheduleAtFixedRate(job.getRunnable(), job.getInterval()));
            }
        } else {
            addFixedRateJob(job);
            this.unresolvedJobs.put(job, scheduledJob);
        }
        return (newJob ? scheduledJob : null);
    }


    /**
     * Schedule the specified fixed-delay task, either right away if possible
     * or on initialization of the scheduler.
     *
     * @return a handle to the scheduled task, allowing to cancel it
     * (or {@code null} if processing a previously registered task)
     */
    
    public ScheduledJob scheduleFixedDelayJob(FixedDelayJob job) {
        ScheduledJob scheduledJob = this.unresolvedJobs.remove(job);
        boolean newJob = false;
        if (scheduledJob == null) {
            scheduledJob = new ScheduledJob(job);
            newJob = true;
        }
        if (this.jobScheduler != null) {
            if (job.getInitialDelay() > 0) {
                Date startTime = new Date(this.jobScheduler.getClock().millis() + job.getInitialDelay());
                scheduledJob.setFuture(this.jobScheduler.scheduleWithFixedDelay(job.getRunnable(), startTime, job.getInterval()));
            } else {
                scheduledJob.setFuture(this.jobScheduler.scheduleWithFixedDelay(job.getRunnable(), job.getInterval()));
            }
        } else {
            addFixedDelayJob(job);
            this.unresolvedJobs.put(job, scheduledJob);
        }
        return (newJob ? scheduledJob : null);
    }


    /**
     * Return all locally registered tasks that have been scheduled by this registrar.
     *
     * @see #addTriggerJob
     * @see #addCronJob
     * @see #addFixedRateJob
     * @see #addFixedDelayJob
     */
    @Override
    public Set<ScheduledJob> getScheduledJobs() {
        return Collections.unmodifiableSet(this.scheduledJobs);
    }

    public void destroy() {
        for (ScheduledJob job : this.scheduledJobs) {
            job.cancel();
        }
        if (this.localExecutor != null) {
            this.localExecutor.shutdownNow();
        }
    }

}

